iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 15
2
自我挑戰組

JavaScript 試煉之旅系列 第 15

JavaScript 閉包(Closure)

  • 分享至 

  • xImage
  •  

閉包(closure)要先懂的一些觀念

  1. JavaScript在執行時變數(variable)與函式(function)的流程
  2. 範疇鍊(scope chain)
  3. 閉包可以保留函式執行結束後,其外層作用域函式的變數

接下來就來一一對於前面三點做個深入的理解

JavaScript在執行時變數與函式的運作

  1. JavaScript在執行時,會將變數、函式存入記憶體(hoisting行為)
  2. 當變數被賦予值時,將值指定給變數
  3. 函式被調用時,會建立自己的執行環境(Execution Context),並將函式中的變數或函式一併存入記憶體中等待後續的執行
  4. 執行完成後,JavaScript 會執行垃圾回收的行為(garbage collection),將執行完成的函式、變數等一併清除。

範疇鍊(scope chain)

當函式的變數在 自己的範疇(scope)中找不到符合的值時,會往外層範疇(scope)尋找,直到找到全域後才停止,而這也稱為範疇鍊(scope chain)。

來看看這個測試例子了解一下範疇鍊(scope chain):

var text = "This is a text";
function getText(){
  console.log(text);
}
getText();

getText 函式中因為沒有變數 text,所以往外層的全域尋 text 變數並使用它的值 This is a text

Day15-1

閉包可以保留函式執行結束後,在該函式裡面的變數

直接透過一個測試例子來看看:

function outerScope(){
  var scope = 'This is outer scope value'; 
  return function(){
    return scope;
  }
}

var getText = outerScope();
console.log(getText);
console.log(getText());

來看看程式碼執行的流程:

  1. JavaScript執行時,將 outerScope 函式 、 getText 變數 存入記憶體中,等待被執行。
  2. 當要取得 getText 變數的值時, outerScope 函式被執行,建立 outerScope 函式的執行環境,回傳內部的函式,到這裡所獲得的是 console.log(getText); 的結果。
  3. outerScope 函式執行完成後會被JavaScript清除,但是 scope 變數會被保留,因為最內層的函式需要在被執行時取得 scope 變數的值。
  4. 而此時的 getText 的值是一個函式,所以當執行這個函式時,裡面的 scope 變數在自己的範疇(scope)找不到這個變數,所以往外層 outerScope 函式中尋找並得到值 This is outer scope value,這裡是console.log(getText()); 的結果。

而這就是一個完整閉包的運作流程。

Day15-2

讓我們再看看一個經典的例子:

function test(){
  var arr =[];
  for(var i = 0; i < 3; i++){
    arr.push(function(){
      console.log(i);
    })
  }
  return arr;
}
var result = test();
result[0]();
result[1]();
result[2]();

此時 result 的值為一組儲存了三個函式的元素,當依序取出函式中的 i 的值時,預期要得到的值分別為 012

但透過下圖可以發現結果卻不如預期:

Day15-3

來試著了解程式執行的操作流程:

  1. JavaScript執行時,將 test 函式 、 result 變數 存入記憶體中,等待被執行。
  2. 當要取得 result 變數的值時, test 函式被執行,建立 test 函式的執行環境,for 迴圈會依序將函式作為元素值存入陣列中,這邊要注意的是作為陣列元素存入的函式還沒有被執行,所以並不會得到值為 012
  3. 當依序執行 result[0]()result[1]()result[2]()時,因為閉包(closure)的觀念,內部函式會取得 for 迴圈的 i 值,此時的 i 值則是已經跑完 for 迴圈後的值,而且因為處於相同的範疇(scope),所以所有的執行結果最後都會拿到值為 3

因為 JS 在ES6之前是 函式範疇(function scope),所以上述的程式碼在每次執行時,i 都是處於同一個範疇(scope)中,進而得到上述的結論。

那要怎麼樣才能得到預期的結果,輸出為 012 呢?

剛剛有提到因為處於同一個範疇(scope)中而導致不是預期的結果,所以是不是 讓每次的 i 值都處於不同的範疇(scope)中就可以解決了?

兩種建立新的範疇(scope)方式:

  1. IIEF 立即函式
  2. ES6之後的 letconst 變數

首先先來看看透過 IIFE 立即函式改寫後的測試例子:

function test(){
  var arr =[];
  for(var i = 0; i < 3; i++){
    arr.push((function(){
      console.log(i);
    })());
  }
  return arr;
}
var result = test();

IIFE 會建立一個新的範疇(scope),所以在每次的 for 迴圈都建立一個新的範疇(scope)就能得到預期的值

Day15-4

再來看看透過 ES6 的 letconst 變數:

function test(){
  var arr =[];
  for(let i = 0; i < 3; i++){
    arr.push(function(){
      console.log(i);
    });
  }
  return arr;
}
var result = test();
result[0]();
result[1]();
result[2]();

因為 letconst 變數會建立 區塊範疇(block scope),所以在每次在執行 for 迴圈時都是處於不同的範疇(scope),所以也能達到預期的結果。

Day15-5

以上就是閉包的一些觀念,今天就先到這裡囉~

明天見~


上一篇
JavaScript 的 this 怎能不知道
下一篇
JavaScript class 類別
系列文
JavaScript 試煉之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言